// Description: 分布式锁，解决多容器环境下并行问题加锁
package dlock

import (
	"context"
	"math/rand"
	"sync"
	"time"

	"github.com/zeromicro/go-zero/core/stores/cache"
	"github.com/zeromicro/go-zero/core/stores/redis"
)

type (
	DLock interface {
		Locker(redisLockKey string, expireSeconds ...int) Locker
	}

	Locker interface {
		Acquire(num ...int) (bool, error)
		AcquireCtx(ctx context.Context, num ...int) (bool, error)
		OnceAcquire() (bool, error)
		OnceAcquireCtx(ctx context.Context) (bool, error)
		Release() (bool, error)
		ReleaseCtx(ctx context.Context) (bool, error)
		SetExpire(seconds int)
	}

	defaultDLock struct {
		client *redis.Redis
	}

	defaultLocker struct {
		lock *redis.RedisLock
	}
)

var (
	_dLock        DLock
	_onceDLock    sync.Once
	defaultExpire = 5 // 默认过期5秒
)

func NewDLock(c cache.CacheConf) DLock {
	_onceDLock.Do(func() {
		_dLock = &defaultDLock{
			client: c[0].NewRedis(),
		}
	})
	return _dLock
}

func (l *defaultDLock) Locker(redisLockKey string, expireSeconds ...int) Locker {
	redisLock := redis.NewRedisLock(l.client, redisLockKey)
	if expireSeconds != nil {
		redisLock.SetExpire(expireSeconds[0])
	} else {
		redisLock.SetExpire(defaultExpire)
	}

	return &defaultLocker{
		lock: redisLock,
	}
}

func (dl *defaultLocker) Acquire(num ...int) (bool, error) {
	return dl.AcquireCtx(context.Background(), num...)
}

func (dl *defaultLocker) AcquireCtx(ctx context.Context, num ...int) (bool, error) {
	n := 10
	if len(num) > 0 {
		n = num[0]
	}

	for i := 0; i < n; i++ {
		ok, err := dl.lock.AcquireCtx(ctx)
		if err != nil {
			return false, err
		}

		if ok {
			return ok, nil
		}

		// 休眠重试
		time.Sleep(15 * time.Millisecond * time.Duration(rand.Intn(4)+1))
	}

	return false, nil
}

func (dl *defaultLocker) OnceAcquire() (bool, error) {
	return dl.OnceAcquireCtx(context.Background())
}

func (dl *defaultLocker) OnceAcquireCtx(ctx context.Context) (bool, error) {
	return dl.lock.AcquireCtx(ctx)
}

func (dl *defaultLocker) Release() (bool, error) {
	return dl.ReleaseCtx(context.Background())
}

func (dl *defaultLocker) ReleaseCtx(ctx context.Context) (bool, error) {
	return dl.lock.ReleaseCtx(ctx)
}

func (dl *defaultLocker) SetExpire(seconds int) {
	dl.lock.SetExpire(seconds)
}
